Skip to content

C++ SDK#1421

Closed
bhardwajparth51 wants to merge 16 commits intoappwrite:masterfrom
bhardwajparth51:feat/cpp-sdk-generator
Closed

C++ SDK#1421
bhardwajparth51 wants to merge 16 commits intoappwrite:masterfrom
bhardwajparth51:feat/cpp-sdk-generator

Conversation

@bhardwajparth51
Copy link
Copy Markdown
Contributor

@bhardwajparth51 bhardwajparth51 commented Apr 4, 2026

What does this PR do?

Adds a modern, production-ready C++ SDK generator targeting the C++20 standard.
This implementation focuses on high performance, thread-safety, and a seamless developer
experience through a monolithic header-only architecture and coroutine-compatible async dispatch.


Key Features

  • Monolithic Header-only: Simple integration with a single include/appwrite/appwrite.hpp entry point. No separate compilation units required.
  • Concurrent Dispatch via ThreadPool: All API calls are dispatched onto a dedicated ThreadPool, enabling concurrent execution across multiple threads. The Task<T> type provides a coroutine-compatible interface (co_await / .get()). Note: true non-blocking I/O would require an async runtime (Asio/libuv), which is intentionally out of scope for this SDK.
  • Robust Transport Layer: Full support for chunked multi-part uploads, binary responses (callBytes), and complex JSON serialization.
  • Topological Model Sorting: Automatically resolves circular dependencies in model definitions at generation time.
  • Thread-Safety: The Client uses a copy-on-write config pattern via shared_ptr<const Config> with mutex protection, making it safe for concurrent use across multiple threads.

Production Hardening

  • Designated initializers for InputFile::Progress (C++20 compliant, order-safe).
  • Error type extraction in verify() — extracts the namespaced "type" field from error JSON and passes it to ServerException for richer error context.
  • Safe coroutine exception wrapping in makeResolvedTask — exceptions captured by std::future are caught and returned as Result<T>::Err rather than propagating as raw exceptions.
  • PHP-style array serialization — GET and Multipart parameters backed by arrays are serialized with the [] suffix (e.g. k[]=v1&k[]=v2).
  • Geospatial Nesting: Corrected to the 4-level array format required by the Appwrite backend for all query types.

Test Plan

  1. Integration Verification: 444 assertions passing across the full Appwrite integration test suite, covering:
    • Full REST lifecycle against the Appwrite Mock API.
    • Large file chunked uploads with x-appwrite-id propagation.
    • Binary response handling and HTTP error model parsing.
    • Query, Permission, and ID helper correctness.
  2. CI Compliance: test.sh executes the suite within the mockapi Docker network. The build-validation CI step compiles headers without running integration tests, keeping CI fast.

Verified output (condensed):

x-sdk-name: cpp; x-sdk-platform: server; x-sdk-language: cpp; x-sdk-version: 0.0.1
GET:/v1/mock/tests/foo:passed
POST:/v1/mock/tests/foo:passed
...
OK (1 test, 444 assertions)

@bhardwajparth51 bhardwajparth51 force-pushed the feat/cpp-sdk-generator branch 8 times, most recently from 76958b4 to 6fe7d9c Compare April 7, 2026 16:02
@bhardwajparth51 bhardwajparth51 marked this pull request as ready for review April 7, 2026 16:08
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 7, 2026

Greptile Summary

This PR adds a C++ SDK generator targeting C++20, including Twig templates, a PHP generator class, generated example output, and a full test harness. The second revision addresses a substantial number of previously flagged issues: ServerException argument order is now correct, upload_chunks/fileUpload return Result::Err on failure, parent::setUp() is called, test.sh correctly passes -DAPPWRITE_RUN_INTEGRATION, GET array parameters are now serialized with [] suffix, Content-Type: application/json is set for POST/PUT/PATCH, and tests_main.cpp.twig now generates a proper GTest main().

  • P1 — Task<T>::get() spin loop causes concurrent handle.resume() UB: the spin loop calls handle.resume() while the pool has already enqueued a second resume() call; two concurrent resumes on the same handle are undefined behaviour and will likely crash (base.hpp.twig line 181).
  • P1 — Result::error() returns a dangling const AppwriteException&: the caught exception object is destroyed when the catch handler exits; every caller receives a dangling reference (base.hpp.twig line 119).
  • P1 — distanceEqual (6-arg) packs compound as single element, producing values with one extra nesting level vs. Base.php's expectation; the QUERY_HELPER_RESPONSES assertion for that line fails on every run (core.hpp.twig line 260).

Confidence Score: 2/5

Not safe to merge — multiple P1 runtime defects remain that will cause crashes and test failures.

Three confirmed P1 issues: (1) concurrent coroutine resume UB in Task::get() will crash async callers; (2) Result::error() returns a dangling reference causing UB on every error inspection; (3) distanceEqual(6-arg) produces wrong JSON nesting causing Cpp20Test to fail. The second revision shows strong improvement — many prior P0/P1 findings resolved — but these three defects block a clean merge.

templates/cpp/include/base.hpp.twig (Task async semantics, dangling reference), templates/cpp/include/core.hpp.twig (distanceEqual nesting), examples/cpp/include/appwrite/core.hpp (same distanceEqual fix needed in generated output)

Important Files Changed

Filename Overview
templates/cpp/include/base.hpp.twig Core types: Result, Task, AppwriteException. Task get() spin loop causes concurrent resume UB; error() returns dangling reference; await_suspend blocks the calling thread making async API synchronous in disguise.
templates/cpp/include/client.hpp.twig Core HTTP client template. ServerException args are now in correct order; array GET params serialized correctly with [] suffix; upload_chunks has try/catch; content-type header set for non-GET; x-appwrite-id propagated.
templates/cpp/include/core.hpp.twig Query, Permission, Role, ID helpers. ID::unique() now generates a 20-char hex string. distanceEqual 6-arg packs compound as single element, producing wrong values nesting depth vs Base.php expectations.
tests/Cpp20Test.php PHP test harness. Now correctly calls parent::setUp() and uses dynamic container name detection; expected output ordering matches integration test output.
tests/languages/cpp/test.sh Shell test driver. Correctly sets APPWRITE_RUN_INTEGRATION and APPWRITE_MOCK_ENDPOINT; separates set +e sections properly; both unit and integration binaries run.
tests/languages/cpp/Dockerfile Pre-builds cpr, nlohmann/json, and googletest from pinned commits. Commit hashes should be documented with their corresponding version tags.
src/SDK/Language/Cpp.php C++ SDK generator. Many filters defined; buildPathLine uses std::format. Several template files still lack getFiles() registrations; enum default value compilation issue for optional enum params remains.
.github/workflows/sdk-build-validation.yml Adds C++ build validation. Pre-installs cpr with CMAKE_PREFIX_PATH, runs cmake with APPWRITE_BUILD_TESTS=ON and parallel build.

Reviews (75): Last reviewed commit: "fix(cpp): add CMAKE_PREFIX_PATH, paralle..." | Re-trigger Greptile

Comment thread templates/cpp/include/client.hpp.twig Outdated
Comment thread src/SDK/Language/Cpp.php
Comment thread templates/cpp/include/services/service.hpp.twig Outdated
Comment thread src/SDK/Language/Cpp.php Outdated
Comment thread templates/cpp/README.md.twig Outdated
Comment thread src/SDK/Language/Cpp.php
Comment thread templates/cpp/include/exception.hpp.twig Outdated
@bhardwajparth51 bhardwajparth51 force-pushed the feat/cpp-sdk-generator branch from 9f55656 to a07a162 Compare April 7, 2026 16:52
Comment thread templates/cpp/include/input_file.hpp.twig Outdated
Comment thread templates/cpp/include/models/model.hpp.twig Outdated
Comment thread templates/cpp/include/services/service.hpp.twig Outdated
Comment thread templates/cpp/include/input_file.hpp.twig Outdated
Comment thread templates/cpp/include/input_file.hpp.twig Outdated
Comment thread templates/cpp/include/services/service.hpp.twig Outdated
Comment thread templates/cpp/include/client.hpp.twig Outdated
Comment thread templates/cpp/include/client.hpp.twig Outdated
@bhardwajparth51
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Comment thread templates/cpp/include/client.hpp.twig Outdated
Comment thread templates/cpp/tests/tests.cpp.twig Outdated
Comment thread src/SDK/Language/Cpp.php Outdated
Comment thread templates/cpp/tests/tests.cpp.twig Outdated
Comment thread templates/cpp/tests/tests.cpp.twig Outdated
@bhardwajparth51 bhardwajparth51 marked this pull request as draft April 13, 2026 19:59
@bhardwajparth51 bhardwajparth51 marked this pull request as ready for review April 13, 2026 20:49
Comment thread tests/languages/cpp/test.sh Outdated
Comment thread src/SDK/Language/Cpp.php
Comment thread templates/cpp/include/services.hpp.twig Outdated
Comment thread templates/cpp/include/services.hpp.twig Outdated
Comment thread templates/cpp/include/models.hpp.twig
Comment thread templates/cpp/include/models.hpp.twig
Comment thread examples/cpp/include/appwrite/services.hpp Outdated
Comment thread templates/cpp/include/services.hpp.twig Outdated
Comment thread templates/cpp/include/base.hpp.twig Outdated
Comment thread templates/cpp/include/client.hpp.twig Outdated
Comment thread example.php Outdated
Comment thread templates/cpp/include/models.hpp.twig Outdated
Comment thread tests/Cpp20Test.php Outdated
Comment thread .github/workflows/sdk-build-validation.yml Outdated
Comment thread templates/cpp/README.md.twig Outdated
Comment thread templates/cpp/README.md.twig Outdated
Comment thread tests/Cpp20Test.php Outdated
Comment thread .github/workflows/sdk-build-validation.yml Outdated
@bhardwajparth51 bhardwajparth51 force-pushed the feat/cpp-sdk-generator branch from 5132d53 to 982469c Compare April 22, 2026 18:46
Comment thread tests/Cpp20Test.php
Comment thread tests/languages/cpp/Dockerfile
Comment thread .github/workflows/sdk-build-validation.yml
Comment thread templates/cpp/include/client.hpp.twig
Comment thread templates/cpp/tests/integration_logic.cpp.twig
@bhardwajparth51 bhardwajparth51 force-pushed the feat/cpp-sdk-generator branch from ba19e27 to 6b7d1f0 Compare April 23, 2026 04:56
Comment thread templates/cpp/include/base.hpp.twig
Comment thread templates/cpp/include/base.hpp.twig
Comment thread examples/cpp/include/appwrite/core.hpp
Comment thread tests/Cpp20Test.php Outdated
Comment on lines 164 to +172
if: matrix.sdk == 'rust'
uses: dtolnay/rust-toolchain@1.83.0

- name: Setup C++
if: matrix.sdk == 'cpp'
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake libcurl4-openssl-dev nlohmann-json3-dev libgtest-dev
git clone --depth 1 --branch 1.10.5 https://github.com/libcpr/cpr.git /tmp/cpr
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 cmake configure step missing -DCMAKE_PREFIX_PATH — cpr may not be found

cpr is installed to /usr/local via cmake --target install, but the subsequent build step runs cmake -S . -B build -DAPPWRITE_BUILD_TESTS=ON without -DCMAKE_PREFIX_PATH=/usr/local. On Ubuntu runners CMAKE_PREFIX_PATH defaults to /usr only, so find_package(cpr QUIET) will fail and FetchContent_MakeAvailable(cpr) will attempt a network clone — adding a GitHub dependency and ~5 min of cpr+libcurl compilation to every CI run. Add the prefix:

cpp)
  cmake -S . -B build -DAPPWRITE_BUILD_TESTS=ON -DCMAKE_PREFIX_PATH=/usr/local
  cmake --build build -j$(nproc)
  ctest --test-dir build --output-on-failure
  ;;

Comment thread .github/workflows/sdk-build-validation.yml Outdated
Comment on lines +181 to +195
struct promise_type {
Task get_return_object() { return Task{std::coroutine_handle<promise_type>::from_promise(*this)}; }
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { result = std::current_exception(); }
template<typename V>
void return_value(V&& value) { result = std::variant<T, std::exception_ptr>(std::forward<V>(value)); }
std::optional<std::variant<T, std::exception_ptr>> result;
};

explicit Task(std::coroutine_handle<promise_type> h) : handle(h) {}
~Task() { if (handle) handle.destroy(); }
Task(Task&& other) noexcept : handle(std::exchange(other.handle, nullptr)) {}
T get() {
if (handle && !handle.done()) handle.resume();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Task<T>::get() spin loop resumes the coroutine handle twice — undefined behaviour

The spin loop calls handle.resume() which runs the coroutine until it suspends at co_await *pool. ThreadPoolAwaiter::await_suspend then enqueues a second h.resume() on the pool and returns — the coroutine is now suspended with a pool-queued resumption pending. The spin loop's !handle.done() check is true, so it calls handle.resume() again immediately. Two concurrent coroutine_handle::resume() invocations on the same handle are undefined behaviour per C++20 [dcl.fct.def.coroutine]. The pool worker's call races with the spin call → likely crash.

Fix: store a std::promise/std::future inside promise_type and block on the future after a single initial resume(), letting the pool be the sole entity calling resume() after first suspension.

Comment on lines +119 to +127
T& operator*() { return value(); }

const T& value() const {
if (isErr()) std::rethrow_exception(std::get<std::exception_ptr>(data_));
return std::get<T>(data_);
}

T& value() {
if (isErr()) std::rethrow_exception(std::get<std::exception_ptr>(data_));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 error() returns a dangling reference — UB on every call

std::rethrow_exception throws the stored exception into the local catch clause. In C++, the caught-exception object lives only until the handler exits; returning e by const& from inside that handler gives the caller a reference to a destroyed object. Every access to the returned reference is undefined behaviour.

// Current (UB):
catch (const AppwriteException& e) { return e; }  // e is destroyed when catch exits

// Fix — return by value:
std::shared_ptr<AppwriteException> error() const {
    if (isOk()) throw AppwriteException("Result is Ok, no error available");
    try { std::rethrow_exception(std::get<std::exception_ptr>(data_)); }
    catch (const AppwriteException& e) { return e.clone(); }
    return std::make_shared<AppwriteException>("Unknown error");
}

Comment on lines +205 to +215
void await_suspend(std::coroutine_handle<> h) noexcept {
task_.handle.resume();
h.resume();
}
T await_resume() {
auto& res = *task_.handle.promise().result;
if (res.index() == 1) std::rethrow_exception(std::get<1>(res));
return std::move(std::get<0>(res));
}
};
// Note: Task is move-only. After co_await, the original Task handle is null.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 co_await task blocks the calling thread — advertised coroutine composability is synchronous in disguise

await_suspend calls task_.handle.resume(), which runs makeResolvedTask until it hits co_return f->get(). f->get() is a blocking call that waits for the thread pool to finish. handle.resume() does not return until the pool completes — the outer coroutine's thread is held the entire time. co_await callAsync(...) is therefore identical in cost and blocking behaviour to calling .get() directly. A non-blocking fix requires await_suspend to enqueue the outer coroutine's handle onto the pool rather than calling handle.resume() inline.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant